/*              Copyright(c) 2008 Dave Sexton              *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Xml;
using System.IO;
using System.Data;
using System.Data.Common;

namespace DaveSexton.Sandcastle
{
	public enum DatabaseParameterSourceType
	{
		Value, 
		BaseUrl, 
		File, 
		Target,
		TopicId
	}

	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable"), 
	DisplayName("Database mappings"), Description("Imports mappings from a database using a custom connection string and query.")]
	public class DatabaseUrlMappingProvider : UrlMappingProvider
	{
		#region Private / Protected
		private DatabaseUrlMappingProviderEditor editor;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="DatabaseUrlMappingProvider" /> class.
		/// </summary>
		public DatabaseUrlMappingProvider()
		{
		}
		#endregion

		#region Methods
		public override System.Windows.Forms.Control GetEditor(UrlMappingGroup group, string configuration)
		{
			if (editor == null || !editor.Created)
				editor = new DatabaseUrlMappingProviderEditor();

			editor.Configuration = configuration;
			return editor;
		}

		public override string GetEditorConfiguration(UrlMappingGroup group, System.Windows.Forms.Control control)
		{
			return editor.Configuration;
		}

		public override IEnumerable<UrlMapping> GetMappings(UrlMappingGroup group, string configuration)
		{
			if (string.IsNullOrEmpty(configuration))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "configuration");

			XmlReaderSettings settings = new XmlReaderSettings();
			settings.IgnoreComments = true;
			settings.IgnoreProcessingInstructions = true;
			settings.IgnoreWhitespace = true;

			using (StringReader input = new StringReader(configuration))
			{
				using (XmlReader reader = XmlReader.Create(input, settings))
				{
					return Execute(group, ReadConfiguration(reader, 1));
				}
			}
		}

		internal static DatabaseUrlMappingProviderSettings ReadConfiguration(XmlReader reader, int settingsDepth)
		{
			DatabaseUrlMappingProviderSettings settings = new DatabaseUrlMappingProviderSettings();

			while (reader.Read())
			{
				if (reader.NodeType == XmlNodeType.Element && reader.Depth == settingsDepth)
				{
					switch (reader.LocalName)
					{
						case "connection":
							if (reader.MoveToAttribute("provider"))
							{
								settings.Provider = reader.Value;
								reader.MoveToElement();
							}

							settings.Connection = reader.ReadString();
							break;
						case "query":
							if (reader.MoveToAttribute("storedProcedure"))
							{
								settings.StoredProcedure = bool.Parse(reader.Value);
								reader.MoveToElement();
							}

							if (reader.MoveToAttribute("timeout"))
							{
								settings.CommandTimeout = int.Parse(reader.Value, System.Globalization.CultureInfo.CurrentCulture);
								reader.MoveToElement();
							}

							if (reader.MoveToAttribute("idColumn"))
							{
								settings.IdColumn = reader.Value;
								reader.MoveToElement();
							}

							if (reader.MoveToAttribute("hrefColumn"))
							{
								settings.HrefColumn = reader.Value;
								reader.MoveToElement();
							}

							settings.Query = reader.ReadString();
							break;
						case "parameters":
							using (XmlReader parametersReader = reader.ReadSubtree())
							{
								// NOTE: The lazy iterator returned by ReadParameters must be evaluated immediately by being added to a List<T>
								// because the XmlReader on which it depends will go out of scope before the parameters are needed during execution.
								settings.Parameters = new List<QueryParameter>(ReadParameters(parametersReader)).AsReadOnly();
							}
							break;
					}
				}
			}

			return settings;
		}

		private static IEnumerable<QueryParameter> ReadParameters(XmlReader reader)
		{
			while (reader.ReadToFollowing("parameter"))
			{
				if (reader.Depth == 1)
				{
					string name = reader.GetAttribute("name");
					string source = reader.GetAttribute("source");
					string value = null;

					if (reader.MoveToAttribute("value"))
					{
						value = reader.Value;
						reader.MoveToElement();
					}
					
					DatabaseParameterSourceType sourceType;

					if (source == null || (source = source.Trim()).Length == 0)
						sourceType = DatabaseParameterSourceType.Value;
					else if (value != null)
						throw new XmlException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							Resources.Errors.DatabaseParameterValueAndSource, name));
					else
					{
						try
						{
							sourceType = (DatabaseParameterSourceType) Enum.Parse(typeof(DatabaseParameterSourceType), source, true);
						}
						catch (ArgumentException ex)
						{
							throw new XmlException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
								Resources.Errors.InvalidDatabaseParameterSourceType, name, Environment.NewLine, source), ex);
						}
					}

					yield return new QueryParameter(name, value, sourceType);
				}
			}
		}

		protected virtual IEnumerable<UrlMapping> Execute(UrlMappingGroup group, DatabaseUrlMappingProviderSettings settings)
		{
			if (string.IsNullOrEmpty(settings.Connection))
				throw new XmlException(Resources.Errors.MissingDatabaseConnectionString);

			if (string.IsNullOrEmpty(settings.Query))
				throw new XmlException(Resources.Errors.MissingDatabaseQuery);

			DbProviderFactory factory = DbProviderFactories.GetFactory(settings.Provider);

			using (DbConnection connection = factory.CreateConnection())
			{
				connection.ConnectionString = settings.Connection;

				using (DbCommand command = factory.CreateCommand())
				{
					PrepareCommand(group, connection, command, settings);

					using (DbDataAdapter adapter = factory.CreateDataAdapter())
					{
						DataTable table = PrepareDataTableAndAdapter(group, adapter, command, settings);

						if (adapter.Fill(table) > 0)
						{
							foreach (DataRow row in table.Rows)
								yield return new UrlMapping((string) row["ID"], (row.IsNull("HREF")) ? null : (string) row["HREF"]);
						}
					}
				}
			}
		}

		protected virtual void PrepareCommand(UrlMappingGroup group, DbConnection connection, DbCommand command, DatabaseUrlMappingProviderSettings settings)
		{
			command.Connection = connection;
			command.CommandText = settings.Query;
			command.CommandType = (settings.StoredProcedure) ? CommandType.StoredProcedure : CommandType.Text;

			if (settings.CommandTimeout != null)
				command.CommandTimeout = settings.CommandTimeout.Value;

			if (settings.Parameters != null)
				AddParameters(group, command, settings.Parameters);
		}

		private static void AddParameters(UrlMappingGroup group, DbCommand command, IEnumerable<QueryParameter> parameters)
		{
			foreach (QueryParameter parameter in parameters)
			{
				DbParameter dbParameter = command.CreateParameter();
				dbParameter.ParameterName = parameter.ParameterName;
				dbParameter.Value = EvaluateParameterSource(group, parameter);

				command.Parameters.Add(dbParameter);
			}
		}

		private static string EvaluateParameterSource(UrlMappingGroup group, QueryParameter parameter)
		{
			switch (parameter.Source)
			{
				case DatabaseParameterSourceType.Value:
					return parameter.ParameterValue;
				case DatabaseParameterSourceType.BaseUrl:
					return group.BaseUri.ToString();
				case DatabaseParameterSourceType.File:
					return group.File;
				case DatabaseParameterSourceType.Target:
					return group.Target;
				case DatabaseParameterSourceType.TopicId:
					return group.TopicId ?? "";
				default:
					throw new XmlException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
						Resources.Errors.InvalidDatabaseParameterSourceType, parameter.ParameterName, Environment.NewLine, parameter.Source));
			}
		}

		protected virtual DataTable PrepareDataTableAndAdapter(UrlMappingGroup group, DbDataAdapter adapter, DbCommand command, DatabaseUrlMappingProviderSettings settings)
		{
			DataTable table = new DataTable("Urls");
			table.Locale = System.Globalization.CultureInfo.CurrentCulture;

			DataColumn column = table.Columns.Add("ID", typeof(string));
			column.AllowDBNull = false;
			table.PrimaryKey = new DataColumn[] { column };

			table.Columns.Add("HREF", typeof(string)).AllowDBNull = true;

			adapter.MissingMappingAction = MissingMappingAction.Ignore;
			adapter.MissingSchemaAction = MissingSchemaAction.Ignore;
			adapter.SelectCommand = command;

			DataTableMapping mapping = adapter.TableMappings.Add("Table", "Urls");
			mapping.ColumnMappings.Add(settings.IdColumn, "ID");
			mapping.ColumnMappings.Add(settings.HrefColumn, "HREF");

			return table;
		}
		#endregion
	}
}
